//
// Copyright (c) 2007-2009 Stuart Morgan.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "CaminoCookieStore.h"

#import "GTMSQLite.h"

static NSString* kCaminoTextCookiePath =
    @"~/Library/Application Support/Camino/cookies.txt";
static NSString* kCaminoSQLCookiePath =
    @"~/Library/Application Support/Camino/cookies.sqlite";

enum CaminoCookieFormat {
  CaminoCookieFormatText,
  CaminoCookieFormatSQL,
};

@interface NSString(CookieAdditions)
// Returns a string representation of boolValue suitable for use in an
// NSHTTPCookie creation dictionary.
+ (NSString*)cookieStringForBool:(BOOL)boolValue;
@end

@implementation NSString(CookieAddition)
+ (NSString*)cookieStringForBool:(BOOL)boolValue {
  return boolValue ? @"TRUE" : @"FALSE";
}
@end

#pragma mark -

@interface CaminoCookieStore (Private)

// Determines which (if any) of the two Camino cookie storage files is present.
- (void)findCaminoCookieFile;

// Returns cookies read from an SQL format (Camino 2+) cookie file.
- (NSArray*)readCookiesFromCaminoCookiesSQLFile:(NSString*)file;

// Returns cookies read from a text format (pre-Camino 2) cookie file.
- (NSArray*)readCookiesFromCaminoCookiesTextFile:(NSString*)file;

// Adds the given cookies to the existing SQL file.
- (BOOL)addCookiesToSQLFile:(NSArray*)cookies;

// Overwrites the old cookie text file with one containing the given cookies.
- (BOOL)writeCookiesToTextFile:(NSArray*)cookies;

@end

#pragma mark -

@implementation CaminoCookieStore

- (id)init {
  if ((self = [super init])) {
    [self findCaminoCookieFile];
  }
  return self;
}

- (void)dealloc {
  [mCookieDB release];
  [mCaminoCookieFilePath release];
  [super dealloc];
}

- (NSArray*)storedCookies {
  if (!mCaminoCookieFilePath) {
    [self setLastErrorWithTitleKey:@"NoProfileTitle"
                        messageKey:@"NoProfileText"];
    return nil;
  }
  NSArray* cookies = nil;
  if (mCaminoCookieFileFormat == CaminoCookieFormatSQL) {
    cookies = [self readCookiesFromCaminoCookiesSQLFile:mCaminoCookieFilePath];
  } else {
    cookies = [self readCookiesFromCaminoCookiesTextFile:mCaminoCookieFilePath];
  }
  if (cookies == nil) {
    [self setLastErrorWithTitleKey:@"CantReadCaminoCookiesTitle"
                        messageKey:@"CantReadCaminoCookiesText"];
  }
  return cookies;
}

- (BOOL)addNewCookies:(NSArray*)newCookies {
  if (!mCaminoCookieFilePath)
    return NO;

  if (mCaminoCookieFileFormat == CaminoCookieFormatSQL) {
    return [self addCookiesToSQLFile:newCookies];
  }
  return [self writeCookiesToTextFile:[self cookies]];
}

#pragma mark -

- (void)findCaminoCookieFile {
  NSFileManager* fm = [NSFileManager defaultManager];

  NSString* path = [kCaminoSQLCookiePath stringByExpandingTildeInPath];
  if ([fm fileExistsAtPath:path]) {
    mCaminoCookieFileFormat = CaminoCookieFormatSQL;
    mCaminoCookieFilePath = [path copy];
    return;
  }
  path = [kCaminoTextCookiePath stringByExpandingTildeInPath];
  if ([fm fileExistsAtPath:path]) {
    mCaminoCookieFileFormat = CaminoCookieFormatText;
    mCaminoCookieFilePath = [path copy];
    return;
  }
}

- (NSArray*)readCookiesFromCaminoCookiesSQLFile:(NSString*)file {
  int errorCode = 0;
  mCookieDB = [[GTMSQLiteDatabase alloc] initWithPath:file
                                      withCFAdditions:YES
                                                 utf8:YES
                                            errorCode:&errorCode];
  if (errorCode != SQLITE_OK) {
    NSLog(@"Database open failed: %d", errorCode);
    return nil;
  }
  GTMSQLiteStatement* getCookiesStatement =
      [GTMSQLiteStatement statementWithSQL:@"SELECT * FROM moz_cookies;"
                                inDatabase:mCookieDB
                                 errorCode:&errorCode];
  if (errorCode != SQLITE_OK) {
    NSLog(@"Statement creation failed: %d", errorCode);
    return nil;
  }
  NSMutableArray* cookies = [NSMutableArray array];
  while ([getCookiesStatement stepRow] != SQLITE_DONE) {
    NSDictionary* sqlDict = [getCookiesStatement resultRowDictionary];
    NSDate* cookieDate =
        [NSDate dateWithTimeIntervalSince1970:[[sqlDict objectForKey:@"expiry"] intValue]];
    if (!cookieDate) {
      NSLog(@"Unable to parse cookie date %@", [sqlDict objectForKey:@"expiry"]);
      continue;
    }
    BOOL secure = [[sqlDict objectForKey:@"isSecure"] boolValue];
    NSDictionary* cookieDict = [NSDictionary dictionaryWithObjectsAndKeys:
               [sqlDict objectForKey:@"host"], NSHTTPCookieDomain,
               [sqlDict objectForKey:@"path"], NSHTTPCookiePath,
        [NSString cookieStringForBool:secure], NSHTTPCookieSecure,
                                   cookieDate, NSHTTPCookieExpires,
               [sqlDict objectForKey:@"name"], NSHTTPCookieName,
              [sqlDict objectForKey:@"value"], NSHTTPCookieValue,
                                               nil];
    NSHTTPCookie* cookie = [NSHTTPCookie cookieWithProperties:cookieDict];
    if (cookie)
      [cookies addObject:cookie];
    else
      NSLog(@"Unable to read cookie: %@", cookieDict);
  }
  [getCookiesStatement finalizeStatement];
  return cookies;
}

- (NSArray*)readCookiesFromCaminoCookiesTextFile:(NSString*)file {
  NSString* fileContents = [NSString stringWithContentsOfFile:file];
  if (!fileContents)
    return nil;

  NSMutableArray* cookies = [NSMutableArray array];
  NSEnumerator* lineEnumerator =
      [[fileContents componentsSeparatedByString:@"\n"] objectEnumerator];
  NSString* line;
  while ((line = [lineEnumerator nextObject])) {
    NSArray* fields = [line componentsSeparatedByString:@"\t"];
    if ([fields count] == 1)
      continue; // comment or blank line
    if ([fields count] != 7) {
      NSLog(@"Unparseable cookie: %@", line);
      continue;
    }
    NSDate* cookieDate =
        [NSDate dateWithTimeIntervalSince1970:[[fields objectAtIndex:4] doubleValue]];
    if (!cookieDate) {
      NSLog(@"Unable to handle cookie date '%@'", [fields objectAtIndex:4]);
      continue;
    }
    NSDictionary* cookieDict = [NSDictionary dictionaryWithObjectsAndKeys:
      [fields objectAtIndex:0], NSHTTPCookieDomain,
      [fields objectAtIndex:2], NSHTTPCookiePath,
      [fields objectAtIndex:3], NSHTTPCookieSecure,
      cookieDate, NSHTTPCookieExpires,
      [fields objectAtIndex:5], NSHTTPCookieName,
      [fields objectAtIndex:6], NSHTTPCookieValue,
      nil];
    NSHTTPCookie* cookie = [NSHTTPCookie cookieWithProperties:cookieDict];
    if (cookie)
      [cookies addObject:cookie];
    else
      NSLog(@"Unable to read cookie: %@\n[%@]", line, cookieDict);
  }
  return cookies;
}

- (BOOL)addCookiesToSQLFile:(NSArray*)cookies {
  NSEnumerator* cookieEnumerator = [cookies objectEnumerator];
  NSHTTPCookie* cookie;
  while ((cookie = [cookieEnumerator nextObject])) {
    NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
    long long int cookieID = [[NSDate date] timeIntervalSince1970] * 1000000LL;
    int expiration = [[cookie expiresDate] timeIntervalSince1970];
    int errorCode = 0;
    GTMSQLiteStatement* insertStatement =
        [GTMSQLiteStatement statementWithSQL:@"INSERT INTO moz_cookies VALUES "
                                             @"(?, ?, ?, ?, ?, ?, ?, 0, 0);"
                                  inDatabase:mCookieDB
                                   errorCode:&errorCode];
    if (errorCode != SQLITE_OK) {
      NSLog(@"Failed to initialize statement; error %d", errorCode);
      [self setLastErrorWithTitleKey:@"CantWriteCaminoCookiesTitle"
                          messageKey:@"CantWriteCaminoCookiesText"];
      [innerPool release];
      return NO;
    }
    [insertStatement bindLongLongAtPosition:1 value:cookieID];
    [insertStatement bindStringAtPosition:2 string:[cookie name]];
    [insertStatement bindStringAtPosition:3 string:[cookie value]];
    [insertStatement bindStringAtPosition:4 string:[cookie domain]];
    [insertStatement bindStringAtPosition:5 string:[cookie path]];
    [insertStatement bindInt32AtPosition:6 value:expiration];
    [insertStatement bindInt32AtPosition:7 value:([cookie isSecure] ? 1 : 0)];
    errorCode = [insertStatement stepRow];
    [insertStatement finalizeStatement];
    [innerPool release];
    if (errorCode != SQLITE_DONE) {
      NSLog(@"Failed to execute statement; error %d", errorCode);
      [self setLastErrorWithTitleKey:@"CantWriteCaminoCookiesTitle"
                          messageKey:@"CantWriteCaminoCookiesText"];
      return NO;
    }
  }
  return YES;
}

- (BOOL)writeCookiesToTextFile:(NSArray*)cookies {
  NSFileHandle* caminoFile =
      [NSFileHandle fileHandleForWritingAtPath:mCaminoCookieFilePath];
  if (!caminoFile) {
    [self setLastErrorWithTitleKey:@"CantWriteCaminoCookiesTitle"
                        messageKey:@"CantWriteCaminoCookiesText"];
    return NO;
  }
  [caminoFile truncateFileAtOffset:0];
  NSEnumerator* cookieEnumerator = [cookies objectEnumerator];
  NSHTTPCookie* cookie;
  while ((cookie = [cookieEnumerator nextObject])) {
    if ([cookie isSessionOnly])
      continue;
    NSString* line = [[NSString alloc] initWithFormat:@"%@\t%@\t%@\t%@\t%.0f\t%@\t%@\n",
      [cookie domain],
      [NSString cookieStringForBool:[[cookie domain] hasPrefix:@"."]],
      [cookie path],
      [NSString cookieStringForBool:[cookie isSecure]],
      [[cookie expiresDate] timeIntervalSince1970],
      [cookie name],
      [cookie value]];
    [caminoFile writeData:[line dataUsingEncoding:NSASCIIStringEncoding]];
    [line release];
  }
  [caminoFile closeFile];
  return YES;
}

@end
